package com.nulldev.util.io;

import static com.nulldev.util.io.IOUtilsInternals.isWhitelistedForDMA;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.AbstractInterruptibleChannel;
import java.nio.charset.Charset;

import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.io.util.BufferManager;
import com.nulldev.util.manage.experiments.Experiments;
import com.nulldev.util.web.HttpServer3.util.HttpServerUtil;

public class IOUtils {

	public static final int MEMORY_ALLOC_BUFFER = IOUtilsInternals.MEMORY_ALLOC_BUFFER;
	public static final int STRING_ALLOC_BUFFER = IOUtilsInternals.STRING_ALLOC_BUFFER;

	public static long copyLarge(final InputStream input, final OutputStream output) throws IOException {
		return copyLarge(input, output, new byte[MEMORY_ALLOC_BUFFER]);
	}

	public static long copyLarge(final InputStream input, final OutputStream output, final long contentLength) throws IOException {
		return copyLarge(input, output, new byte[MEMORY_ALLOC_BUFFER], contentLength);
	}

	public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		Variables.requireNonNull(buffer, "buffer");
		if (input == null) {
			return 0L;
		}
		long count = 0L;
		int n = 0;
		while (-1 != (n = input.read(buffer))) {
			output.write(buffer, 0, n);
			count += n;
		}
		return count;
	}

	public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer, final long contentLength) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		Variables.requireNonNull(buffer, "buffer");
		if (input == null || contentLength <= 0) {
			return 0L;
		}
		long left = contentLength;
		int n = 0;
		while (-1 != (n = input.read(buffer)) && left > 0) {
			output.write(buffer, 0, n);
			left -= n;
			if (left <= 0)
				break;
		}
		return left;
	}

	public static void autoCopy(final InputStream input, final OutputStream output) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		if (__USE_FAST_COPY) {
			fastCopy(input, output);
		} else {
			copy(input, output);
		}
	}

	public static void autoCopyWithBufferSize(final InputStream input, final OutputStream output, final int bufrSize) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		__validate_buffer_size(bufrSize);
		/*
		 * Only bother with fastCopy if the buffer is larger than or equal to
		 * MEMORY_ALLOC_BUFFER.
		 */
		if (__USE_FAST_COPY && bufrSize >= MEMORY_ALLOC_BUFFER) {
			fastCopy(input, output);
		} else {
			copy(input, output, new byte[bufrSize]);
		}
	}

	public static void autoCopy(final InputStream input, final OutputStream output, final long contentLength) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		/*
		 * Only bother with fastCopy if the content length is greater than or equal to
		 * MEMORY_ALLOC_BUFFER.
		 */
		if (__USE_FAST_COPY && contentLength >= MEMORY_ALLOC_BUFFER) {
			fastCopy(input, output, contentLength);
		} else {
			copy(input, output, contentLength);
		}
	}

	public static int copy(final InputStream input, final OutputStream output) throws IOException {
		final long count = copy(input, output, new byte[IOUtils.MEMORY_ALLOC_BUFFER]);
		if (count > 2147483647L) {
			return -1;
		}
		return (int) count;
	}

	public static int copy(final InputStream input, final OutputStream output, final byte[] buf) throws IOException {
		final long count = copyLarge(input, output, buf);
		if (count > 2147483647L) {
			return -1;
		}
		return (int) count;
	}

	public static int copy(final InputStream input, final OutputStream output, final long contentLength) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(output, "output");
		final long count = copyLarge(input, output, contentLength);
		if (count > 2147483647L) {
			return -1;
		}
		return (int) count;
	}

	private static void __validate_buffer_size(final int buf) {
		if (buf < 8 || buf > (1 * 1024 * 1024))
			throw new IllegalArgumentException("Invalid buffer size: " + buf);
	}

	private static final boolean __USE_FAST_COPY = canUseFastCopy();

	public static byte[] toByteArray(final InputStream input) throws IOException {
		Variables.requireNonNull(input, "input");
		try {
			final ByteArrayOutputStream output = new ByteArrayOutputStream(MEMORY_ALLOC_BUFFER);
			if (__USE_FAST_COPY) {
				fastCopy(input, output);
			} else {
				copy(input, output);
			}
			final byte[] _o = output.toByteArray();
			IOUtils.closeQuietly(output);
			return _o;
		} catch (IOException e) {
			throw e;
		}
	}

	public static byte[] toByteArray_NoAccel(final InputStream input) throws IOException {
		Variables.requireNonNull(input, "input");
		try {
			final ByteArrayOutputStream output = new ByteArrayOutputStream(MEMORY_ALLOC_BUFFER);
			copy(input, output);
			final byte[] _o = output.toByteArray();
			output.close();
			return _o;
		} catch (IOException e) {
			throw e;
		}
	}

	public static void readFully(InputStream inp, byte[] destination) throws IOException {
		readFully(inp, destination, 0, destination.length);
	}

	public static void readFully(RandomAccessFile inp, byte[] destination) throws IOException {
		readFully(inp, destination, 0, destination.length);
	}

	public static byte[] readFully(final InputStream input, final int offset, final int len) throws IOException {
		Variables.requireNonNull(input, "input");
		if (len < 0)
			throw new IllegalArgumentException("Cannot create a negative-sized array!");
		final byte[] array = new byte[len];
		if (len < 0 || offset < 0 || len + offset > array.length) {
			throw new IndexOutOfBoundsException();
		}
		int count = 0, x = 0;
		while (count != len) {
			x = input.read(array, offset + count, len - count);
			if (x == -1)
				break;
			count += x;
		}
		return array;
	}

	public static byte[] readFully(final RandomAccessFile input, final int offset, final int len) throws IOException {
		Variables.requireNonNull(input, "input");
		if (len < 0)
			throw new IllegalArgumentException("Cannot create a negative-sized array!");
		final byte[] array = new byte[len];
		if (len < 0 || offset < 0 || len + offset > array.length) {
			throw new IndexOutOfBoundsException();
		}
		int count = 0, x = 0;
		while (count != len) {
			x = input.read(array, offset + count, len - count);
			if (x == -1)
				break;
			count += x;
		}
		return array;
	}

	public static int readFully(final InputStream input, final byte[] array, final int offset, final int len) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(array, "array");
		if (len < 0 || offset < 0 || len + offset > array.length) {
			throw new IndexOutOfBoundsException();
		}
		int count = 0, x = 0;
		while (count != len) {
			x = input.read(array, offset + count, len - count);
			if (x == -1)
				break;
			count += x;
		}
		return count;
	}

	public static int readFully(final RandomAccessFile input, final byte[] array, final int offset, final int len) throws IOException {
		Variables.requireNonNull(input, "input");
		Variables.requireNonNull(array, "array");
		if (len < 0 || offset < 0 || len + offset > array.length) {
			throw new IndexOutOfBoundsException();
		}
		int count = 0, x = 0;
		while (count != len) {
			x = input.read(array, offset + count, len - count);
			if (x == -1)
				break;
			count += x;
		}
		return count;
	}

	/**
	 * @param length The length in BYTES
	 */
	public static String getUTF8(final byte[] data, final int offset, final int length) {
		Variables.requireNonNull(data, "data");
		return new String(data, offset, length, UTF8);
	}

	/**
	 * Strips off any null padding, if any, from the string
	 */
	public static String removeNullPadding(final String str) {
		Variables.requireNonNull(str, "str");
		int idx = str.indexOf(0);
		if (idx == -1) {
			return str;
		}
		return str.substring(0, idx);
	}

	/**
	 * Checks to see if the wanted byte pattern is found in the within bytes from
	 * the given offset
	 * 
	 * @param wanted       Byte sequence to look for
	 * @param within       Bytes to find in
	 * @param withinOffset Offset to check from
	 */
	public static boolean byteRangeMatches(final byte[] wanted, final byte[] within, final int withinOffset) {
		Variables.requireNonNull(wanted, "wanted");
		Variables.requireNonNull(within, "within");
		for (int i = 0; i < wanted.length; i++) {
			if (wanted[i] != within[i + withinOffset])
				return false;
		}
		return true;
	}

	protected static final Charset UTF8 = CharsetUtil.UTF_8;

	public static void closeQuietly(final Reader input) {
		closeQuietly((Closeable) input);
	}

	public static void closeQuietly(final Writer output) {
		closeQuietly((Closeable) output);
	}

	public static void closeQuietly(final InputStream input) {
		closeQuietly((Closeable) input);
	}

	public static void closeQuietly(final OutputStream output) {
		closeQuietly((Closeable) output);
	}

	public static void closeQuietly(final Closeable closeable) {
		try {
			if (closeable != null) {
				closeable.close();
			}
		} catch (Exception ex) {
		}
	}

	public static void closeQuietly(final AutoCloseable closeable) {
		try {
			if (closeable != null) {
				closeable.close();
			}
		} catch (Exception ex) {
		}
	}

	public static void closeAllQuietly(final Closeable... closeables) {
		for (final Closeable closeable : closeables) {
			try {
				if (closeable != null) {
					closeable.close();
				}
			} catch (Exception ex) {
			}
		}
	}

	public static void closeAllQuietly(final AutoCloseable... closeables) {
		for (final AutoCloseable closeable : closeables) {
			try {
				if (closeable != null) {
					closeable.close();
				}
			} catch (Exception ex) {
			}
		}
	}

	private static class FixedSizeReadableByteChannelIO extends AbstractInterruptibleChannel implements ReadableByteChannel {
		final InputStream in;
		private static final int TRANSFER_SIZE = IOUtils.MEMORY_ALLOC_BUFFER;
		private volatile byte buf[] = new byte[0];
		private volatile boolean open = true;
		private volatile Object readLock = new Object();
		private long left;

		public FixedSizeReadableByteChannelIO(final InputStream in, final long left) {
			Variables.requireNonNull(in, "in");
			this.left = left;
			this.in = new InputStream() {

				@Override
				public int read() throws IOException {
					return in.read();
				}

				@Override
				public int read(final byte[] b) throws IOException {
					return in.read(b);
				}

				@Override
				public int read(final byte b[], final int off, final int len) throws IOException {
					return in.read(b, off, len);
				}

				public void close() throws IOException {
					in.close();
					FixedSizeReadableByteChannelIO.this.open = false;
					super.close();
				}

				@Override
				public synchronized void mark(final int readlimit) {
					in.mark(readlimit);
				}

				@Override
				public boolean markSupported() {
					return in.markSupported();
				}

				@Override
				public long skip(final long n) throws IOException {
					return in.skip(n);
				}

				@Override
				public synchronized void reset() throws IOException {
					in.reset();
				}

				@Override
				public int available() throws IOException {
					return in.available();
				}
			};
		}

		public int read(final ByteBuffer dst) throws IOException {
			int len = dst.remaining();
			int totalRead = 0;
			int bytesRead = 0;
			synchronized (readLock) {
				while (totalRead < len && left > 0) {
					int bytesToRead = Math.min((len - totalRead), TRANSFER_SIZE);
					if (buf.length < bytesToRead)
						buf = new byte[bytesToRead];
					if ((totalRead > 0) && !(in.available() > 0))
						break; // block at most once
					try {
						begin();
						bytesRead = in.read(buf, 0, bytesToRead);
					} finally {
						end(bytesRead > 0);
					}
					if (bytesRead < 0)
						break;
					else {
						totalRead += bytesRead;
						left -= bytesRead;
					}
					dst.put(buf, 0, bytesRead);
					if (left <= 0)
						break;
				}
				if ((bytesRead < 0) && (totalRead == 0))
					return -1;

				return totalRead;
			}
		}

		protected void implCloseChannel() throws IOException {
			in.close();
			open = false;
		}

		@SuppressWarnings("unused")
		public boolean isClosed() {
			return !this.open;
		}

	}

	public static class FixedSizeWritableByteChannelIO extends AbstractInterruptibleChannel implements WritableByteChannel {
		final OutputStream out;
		private static final int TRANSFER_SIZE = IOUtils.MEMORY_ALLOC_BUFFER;
		private byte buf[] = new byte[0];
		private boolean open = true;
		private final Object writeLock = new Object();
		private long limit;

		public FixedSizeWritableByteChannelIO(final OutputStream out, final long limit) {
			this.limit = limit;
			this.out = new OutputStream() {

				@Override
				public void write(final int b) throws IOException {
					out.write(b);
				}

				@Override
				public void write(final byte[] b) throws IOException {
					out.write(b);
				}

				@Override
				public void write(final byte b[], final int off, final int len) throws IOException {
					out.write(b, off, len);
				}

				@Override
				public void close() throws IOException {
					FixedSizeWritableByteChannelIO.this.open = false;
					out.close();
					super.close();
				}

				@Override
				public void flush() throws IOException {
					out.flush();
				}
			};
		}

		public int write(final ByteBuffer src) throws IOException {
			int len = src.remaining();
			int totalWritten = 0;
			synchronized (writeLock) {
				while (totalWritten < len && limit > 0) {
					int bytesToWrite = Math.min((len - totalWritten), TRANSFER_SIZE);
					if (buf.length < bytesToWrite)
						buf = new byte[bytesToWrite];
					src.get(buf, 0, bytesToWrite);
					try {
						begin();
						out.write(buf, 0, bytesToWrite);
					} finally {
						end(bytesToWrite > 0);
					}
					totalWritten += bytesToWrite;
					limit -= bytesToWrite;
					if (limit <= 0)
						break;
				}
				return totalWritten;
			}
		}

		protected void implCloseChannel() throws IOException {
			out.close();
			open = false;
		}

		public boolean isClosed() {
			return !this.open;
		}
	}

	public static void fastCopy(final InputStream src, final OutputStream dest) throws IOException {
		fastCopy(Channels.newChannel(src), Channels.newChannel(dest));
	}

	public static void fastCopy(final InputStream src, final OutputStream dest, final long contentLength) throws IOException {
		fastCopy(new FixedSizeReadableByteChannelIO(src, contentLength), new FixedSizeWritableByteChannelIO(dest, contentLength));
	}

	public static final boolean canUseFastCopy() {
		return isWhitelistedForDMA() || Experiments.get("io://enable-fast-copy/").isActive();
	}

	public static final boolean canUseDMA() {
		return isWhitelistedForDMA() || Experiments.get("io://nio-use-dma/").isActive();
	}

	public static final boolean canUseMMAP() {
		return Experiments.get("io://nio-use-mmaps/").isActive();
	}

	private static final BufferManager bufMan = new BufferManager(MEMORY_ALLOC_BUFFER, 8);

	public static void fastCopy(final ReadableByteChannel src, final WritableByteChannel dest) throws IOException {
		Variables.requireNonNull(src, "src");
		Variables.requireNonNull(dest, "dest");
		bufMan.awaitFree().then((buffer) -> {
			try {
				while (src.read(buffer) != -1) {
					buffer.flip();
					do {
						dest.write(buffer);
					} while (buffer.hasRemaining());
					buffer.clear();
				}
			} catch (Throwable t) {
				if (!HttpServerUtil.isIgnorableException(t))
					t.printStackTrace();
			}
		}).free();
	}

	public static long getLength(final InputStream inputStream) throws IOException {
		Variables.requireNonNull(inputStream, "inputStream");
		final byte[] EMPTY = new byte[MEMORY_ALLOC_BUFFER];
		long x = 0;
		while (true) {
			final int read = inputStream.read(EMPTY);
			if (read == -1)
				break;
			x += read;
		}
		return x;
	}

	public static ReadableByteChannel copyMakeChannel(final InputStream is) {
		return Channels.newChannel(is);
	}

	public static ReadableByteChannel copyMakeChannelFixed(final InputStream is, final long size) {
		return new FixedSizeReadableByteChannelIO(is, size);
	}

	public static WritableByteChannel copyMakeChannel(final OutputStream os) {
		return Channels.newChannel(os);
	}

	public static WritableByteChannel copyMakeChannelFixed(final OutputStream os, final long size) {
		return new FixedSizeWritableByteChannelIO(os, size);
	}

	public static String readerToString(final Reader reader) {
		try {
			final char[] cbuf = new char[128];
			final StringBuilder builder = new StringBuilder();
			int numChars;
			while ((numChars = reader.read(cbuf)) >= 0) {
				builder.append(cbuf, 0, numChars);
			}
			return builder.toString();
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	public static void flushAndCloseQuietly(final Socket socket) {
		try {
			socket.getOutputStream().flush();
		} catch (Exception e) {
		}
		closeQuietly(socket);
	}

	public static void flushQuietly(final Flushable flushable) {
		try {
			flushable.flush();
		} catch (Exception _ignored) {
		}
	}

	public static void flushQuietly(final OutputStream os) {
		try {
			os.flush();
		} catch (Exception _ignored) {
		}
	}

	public static BufferManager getBufferManager() {
		/* WARNING: Very dangerous operation, you shouldn't touch this */
		return bufMan;
	}

	public static InputStream nonCloseable(final InputStream is) {
		Variables.requireNonNull(is, "is");
		return new InputStream() {

			@Override
			public int read() throws IOException {
				return is.read();
			}

			@Override
			public int read(final byte[] b) throws IOException {
				return is.read(b);
			}

			@Override
			public int read(final byte[] b, final int off, final int len) throws IOException {
				return is.read(b, off, len);
			}

			@Override
			public int available() throws IOException {
				return is.available();
			}

			@Override
			public void close() throws IOException {
				/* NO-OP */
			}

			@Override
			public synchronized void mark(final int readlimit) {
				is.mark(readlimit);
			}

			@Override
			public boolean markSupported() {
				return is.markSupported();
			}

			@Override
			public synchronized void reset() throws IOException {
				is.reset();
			}

			@Override
			public long skip(final long n) throws IOException {
				return is.skip(n);
			}
		};
	}

	public static OutputStream nonCloseable(final OutputStream os) {
		Variables.requireNonNull(os, "os");
		return new OutputStream() {

			@Override
			public void write(final int b) throws IOException {
				os.write(b);
			}

			@Override
			public void close() throws IOException {
				/* NO-OP */
			}

			@Override
			public void flush() throws IOException {
				os.flush();
			}

			@Override
			public void write(byte[] b) throws IOException {
				os.write(b);
			}

			@Override
			public void write(byte[] b, int off, int len) throws IOException {
				os.write(b, off, len);
			}

			@Override
			public int hashCode() {
				return os.hashCode();
			}

			@Override
			public String toString() {
				return os.toString();
			}

			@Override
			public boolean equals(Object o) {
				return os.equals(o);
			}
		};
	}

	public static void closeInputQuietly(final Socket socket) {
		try {
			socket.shutdownInput();
		} catch (Exception ex) {
		}
	}

	public static void closeOutputQuietly(final Socket socket) {
		try {
			socket.shutdownOutput();
		} catch (Exception _ignored) {
		}
	}

	public static int readInputStreamWithTimeout(final InputStream is, final byte[] b, final int timeoutMillis) throws IOException {
		Variables.requireNonNull(is, "is");
		Variables.requireNonNull(b, "b");

		int bufferOffset = 0;
		long maxTimeMillis = System.currentTimeMillis() + timeoutMillis;
		while (System.currentTimeMillis() < maxTimeMillis && bufferOffset < b.length) {
			int readLength = java.lang.Math.min(is.available(), b.length - bufferOffset);
			// can alternatively use bufferedReader, guarded by isReady():
			int readResult = is.read(b, bufferOffset, readLength);
			if (readResult == -1)
				break;
			bufferOffset += readResult;
		}
		return bufferOffset;
	}
}
